//DreamSpace.using(DreamSpace.Modules.Ilinsky_Com_XMLHttpRequest, false, null);

module DreamSpace.System {

    /** Contains a series of core DreamSpace communication based GraphItem derived classes. */
    export module Net {
        // ===================================================================================================================

        /** The base class for all networkable objects. */
        export class NetworkObject extends Object.$Type implements IDisposable, ISerializable {
            /** A channel that this object belongs to for receiving network data. */
            __channelName: string

            /** Creates a networkable object instance.
            * @param {string} channelName A name to group network objects by.
            *                             Since network object IDs are unique across the entire JavaScript environment, normally a new channel isn't required;
            *                             However, it may be that too many objects in one group may cause performance issues.  For this reason (and many
            *                             others), you can split up objects of like kind into their own channel groups.
            *                             Another reason for channels is for UDP communication.  In TCP, data is received in order; however, there can be
            *                             performance hits if packets come out of order, as a less-important packet may block a more important one.  With
            *                             channels, if any packets are received out of order, or get lost, proper grouping means less blocking of critical
            *                             packets.
            */
            constructor(channelName: string = "default") {
                super();
                this.__channelName = channelName || "default";
                var objects = channels[this.__channelName];
                if (!objects) channels[this.__channelName] = objects = new Collections.IndexedObjectCollection<NetworkObject>();
                objects.addObject(this); // (add first so we have the '__id' value if needed)
            }

            dispose(): void {
                var objects = channels[this.__channelName];
                if (objects)
                    objects.removeObject(this);
                DreamSpace.dispose(this);
            }

            getData(data: SerializedData): void {
                data.addValue('__id', this.__id);
            }

            setData(data: SerializedData): void {
            }
        }

        /** A list of all networkable objects for remote tracking, categorized in communication "channels". */
        export var channels: { [channelName: string]: Collections.IndexedObjectCollection<NetworkObject> };

        // ===================================================================================================================

        export enum DataClientTypes {
            AutoDetect,
            AJAX,
            WebSocket,
            SOAP
        }

        export enum DataTypes {
            /** Data sent/received is unknown. */
            Any = <DataTypes><any>null,
            ArrayBuffer = <DataTypes><any>"arraybuffer",
            Blob = <DataTypes><any>"blob",
            Document = <DataTypes><any>"document",
            JSON = <DataTypes><any>"json",
            Text = <DataTypes><any>"text"
        }

        export enum ReadyStates {
            UNSENT,
            OPENED,
            HEADERS_RECEIVED,
            LOADING,
            DONE,
        }

        export enum HTTPReponseCodes {
            /**  */
            CONTINUE = 100,
            SWITCHING_PROTOCOLS = 101,
            OK = 200,
            CREATED = 201,
            ACCEPTED = 202,
            NON_AUTHORITATIVE_INFORMATION = 203,
            NO_CONTENT = 204,
            RESET_CONTENT = 205,
            PARTIAL_CONTENT = 206,
            MULTIPLE_CHOICES = 300,
            MOVED_PERMANENTLY = 301,
            MOVED_TEMPORARILY = 302,
            FOUND = 302,
            SEE_OTHER = 303,
            NOT_MODIFIED = 304,
            USE_PROXY = 305,
            TEMPORARY_REDIRECT = 307,
            BAD_REQUEST = 400,
            UNAUTHORIZED = 401,
            PAYMENT_REQUIRED = 402,
            FORBIDDEN = 403,
            NOT_FOUND = 404,
            METHOD_NOT_ALLOWED = 405,
            NOT_ACCEPTABLE = 406,
            PROXY_AUTHENTICATION_REQUIRED = 407,
            REQUEST_TIMEOUT = 408,
            CONFLICT = 409,
            GONE = 410,
            LENGTH_REQUIRED = 411,
            PRECONDITION_FAILED = 412,
            REQUEST_ENTITY_TOO_LARGE = 413,
            REQUEST_URI_TOO_LONG = 414,
            UNSUPPORTED_MEDIA_TYPE = 415,
            REQUESTED_RANGE_NOT_SATISFIABLE = 416,
            EXPECTATION_FAILED = 417,
            INTERNAL_SERVER_ERROR = 500,
            NOT_IMPLEMENTED = 501,
            BAD_GATEWAY = 502,
            SERVICE_UNAVAILABLE = 503,
            GATEWAY_TIMEOUT = 504,
            HTTP_VERSION_NOT_SUPPORTED = 505
        }

        // ===================================================================================================================

        /** Represents a client side data communications object. 
        * The connection URI determines the method.  If the URI starts with "ws://" or "wss://", then web sockets are assumed, otherwise AJAX communication
        * is assumed, and the URL represents a resource location.
        */
        export class DataClient extends Object.$Type implements IDisposable {

            // ---------------------------------------------------------------------------------------------------------------

            xmlHttpRequest: XMLHttpRequest = null; // (in support of AJAX)
            webSocket: WebSocket = null; // (in support of web sockets)

            /** Set to false to cause blocking to occur when calling 'connect()' for AJAX requests (note: this has no affect on sockets). */
            asyncRequest: boolean;

            connectionURI: string;

            /** The method for getting data when using XMLHttpRequest (AJAX) connections (i.e. 'GET', 'POST', 'PUT', or 'DELETE'); The default is 'get'. */
            xmlHttpMethod: string = "get";

            /** A username for XMLHttpRequest (AJAX) connections, if applicable. */
            userName: string;
            /** A password for XMLHttpRequest (AJAX) connections, if applicable. */
            password: string;

            onOpen = EventDispatcher<DataClient, (client: DataClient, $e?: EventDispatcher.$Type<DataClient, EventHandler>) => any>(this, "onopen", false);
            onError = EventDispatcher<DataClient, (client: DataClient, ev: Event, $e?: EventDispatcher.$Type<DataClient, EventHandler>) => any>(this, "onerror", false);
            onProgress = EventDispatcher<DataClient, (client: DataClient, total: number, loaded: number, $e?: EventDispatcher.$Type<DataClient, EventHandler>) => any>(this, "onprogress", false);
            onData = EventDispatcher<DataClient, (client: DataClient, $e?: EventDispatcher.$Type<DataClient, EventHandler>) => any>(this, "ondata", false);
            onClosed = EventDispatcher<DataClient, (client: DataClient, $e?: EventDispatcher.$Type<DataClient, EventHandler>) => any>(this, "onclosed", false);

            data: any = null;
            dataType: DataTypes = DataTypes.Any;
            status: number = 0;
            errorMessage: string = "";

            clientType: DataClientTypes = DataClientTypes.AutoDetect;

            /** Total bytes (-1 means 'not yet known, or unknown'). */
            total: number = -1;
            /** Total bytes loaded (-1 means 'not yet known, or unknown'). */
            loaded: number = -1;

            /** A reference to the last event that occurred. */
            lastEvent: Event = null; // (total bytes loaded [-1 means 'unknown'])

            // ---------------------------------------------------------------------------------------------------------------

            private __timeout: number = 60000;
            private __timeoutHandle: number;

            // ---------------------------------------------------------------------------------------------------------------

            /** Creates a new instance of the DataClient object.
            * @param {GraphItem} parent The parent of this GraphItem object (usually the application object).
            * @param {string} connectionURI The URI of the resource data to load.
            * @param {boolean} asyncRequest The URI of the resource data to load.
            */
            constructor(connectionURI: string = null, asyncRequest: boolean = true) {
                super();
                this.connectionURI = connectionURI;
                this.asyncRequest = asyncRequest;
            }

            /** Disposes this object and releases resources and/or event hooks. */
            dispose(): void {
                this.disconnect();
                this.xmlHttpRequest = null; // (in support of AJAX)
                this.webSocket = null; // (in support of web sockets)
                this.data = null;
                dispose([this.onOpen, this.onError, this.onProgress, this.onData, this.onClosed]); // (release the events, and any DOM hooks)
            }

            /** Sets the connection/request timeout. */
            setTimeout(ms: number) {
                this.__timeout = ms;
                if (this.xmlHttpRequest)
                    this.xmlHttpRequest.timeout = ms;
            }

            disconnect(): void {
                if (this.__timeoutHandle !== void 0) {
                    clearTimeout(this.__timeoutHandle);
                    this.__timeoutHandle = void 0;
                }

                // ... disconnect any current operations in progress ...
                // (https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest?redirectlocale=en-US&redirectslug=DOM%2FXMLHttpRequest)
                // (https://developer.mozilla.org/en-US/docs/Web/API/WebSocket?redirectlocale=en-US&redirectslug=WebSockets%2FWebSockets_reference%2FWebSocket)

                if (this.xmlHttpRequest
                    && this.xmlHttpRequest.readyState >= 1 /*OPENED*/
                    && this.xmlHttpRequest.readyState < 4/*DONE*/) {
                    log.write("Previous AJAX connection was terminated.", Diagnostics.LogTypes.Warning);
                    this.xmlHttpRequest.abort();
                }
                else if (this.webSocket && this.webSocket.readyState >= 0 /*CONNECTING; 1=OPENED*/
                    && this.xmlHttpRequest.readyState < 2/*CLOSING; 3=CLOSED*/) {
                    log.write("Previous web sockets connection was terminated.", Diagnostics.LogTypes.Warning);
                    this.webSocket.close(4000, "Reconnect requested.");
                }
            }

            // ---------------------------------------------------------------------------------------------------------------

            /** Initiates the connection to the Socket or AJAX request. */
            connect(connectionURI?: string, asyncRequest?: boolean, clientType?: DataClientTypes) {
                this.connectionURI = connectionURI || this.connectionURI;
                this.asyncRequest = asyncRequest || this.asyncRequest;
                this.errorMessage = "";
                this.total = -1;
                this.loaded = -1;
                this.lastEvent = null;
                this.data = null;
                this.status = 0;

                var log = Diagnostics.log(Modules.$_System.Net, "Request to connect; URL: " + this.connectionURI, Diagnostics.LogTypes.Info).beginCapture();

                if (!this.connectionURI)
                    throw Exception.from("The 'connectionURI' value is empty or invalid: '" + this.connectionURI + "'", this);

                this.disconnect();

                if (!clientType && !this.clientType) {
                    var protocol = this.connectionURI.substring(0, this.connectionURI.indexOf("://")).toLowerCase();

                    if (protocol == "ws" || protocol == "wss")
                        clientType = DataClientTypes.WebSocket;
                    else if (protocol == "soap")
                        clientType = DataClientTypes.SOAP;
                    else
                        clientType = DataClientTypes.AJAX;
                }

                if (clientType == DataClientTypes.WebSocket) {
                    this.xmlHttpRequest = null;
                    log.write("Opening a web sockets connection ...");

                    // (see: http://cjihrig.com/blog/how-to-use-websockets/)
                    this.webSocket = new WebSocket(this.connectionURI);
                    this.webSocket.onopen = (ev: Event) => {
                        this.lastEvent = ev;
                        if (this.__timeoutHandle !== void 0) {
                            clearTimeout(this.__timeoutHandle);
                            this.__timeoutHandle = void 0;
                        }
                        this.onOpen.setTriggerState() && this.onOpen.dispatch(this);
                    };
                    this.webSocket.onerror = (ev: ErrorEvent) => {
                        this.lastEvent = ev;
                        this.errorMessage = ev.message;
                        Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": " + this.errorMessage, Diagnostics.LogTypes.Error);
                        this.onError.setTriggerState() && this.onError.dispatch(this, ev);
                    };
                    this.webSocket.onmessage = (ev: any) => {
                        this.lastEvent = ev;
                        this.data = ev.data;
                        Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Data received: \"" + this.data + "\"", Diagnostics.LogTypes.Success);
                        this.dataType = <DataTypes><any>this.webSocket.binaryType || DataTypes.Text;
                        this.onData.setTriggerState() && this.onData.dispatch(this);
                    };
                    this.webSocket.onclose = (ev: CloseEvent) => {
                        this.lastEvent = ev;
                        if (!ev.wasClean) {
                            this.errorMessage = ev.reason;
                            this.onError.setTriggerState() && this.onError.dispatch(this, ev);
                        }
                        this.onClosed.setTriggerState() && this.onClosed.dispatch(this);
                    };

                    if (this.__timeout)
                        this.__timeoutHandle = setTimeout(() => {
                            this.webSocket.close();
                            this.errorMessage = "Connection timed out.";
                            this.onError.setTriggerState("timeout") && this.onError.dispatch(this, null);
                        }, this.__timeout);

                    Diagnostics.log(Modules.$_System.Net, "Web sockets connection opening; ready for data ...");

                } else { // (note: SOAP uses AJAX, but XML is expected instead of JSON[P])
                    this.webSocket = null;
                    log.write("Opening an AJAX connection ...");

                    if (clientType == DataClientTypes.SOAP)
                        if (host.isStudio() || host.isServer()) {
                            // ... use the host bridge to read data from the URI (cross-domain is not supported in browser environments) ...
                            return;
                        }

                    this.xmlHttpRequest = new XMLHttpRequest();

                    this.xmlHttpRequest.onerror = (ev: ErrorEvent) => {
                        this.lastEvent = ev;
                        this.errorMessage = ev.message;
                        Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": " + this.errorMessage, Diagnostics.LogTypes.Error);
                        this.onError.setTriggerState() && this.onError.dispatch(this, ev);
                    };
                    this.xmlHttpRequest.onload = (ev: Event) => {
                        this.lastEvent = ev;
                        /** (called when data download is completed) */
                        if (!this.data) { // (set data if not already set)
                            this.data = this.xmlHttpRequest.response || this.xmlHttpRequest.responseText || this.xmlHttpRequest.responseXML || this.xmlHttpRequest.responseBody;
                            this.status = this.xmlHttpRequest.status;
                        }
                        if (this.onData.setTriggerState()) {
                            if (this.status == 200) {
                                Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Data loaded from server (status: " + this.xmlHttpRequest.status + "): \"" + this.data + "\"", Diagnostics.LogTypes.Success);
                                this.onData.dispatch(this);
                            } else if (this.status == 304) {
                                Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Data loaded from cache (status: " + this.xmlHttpRequest.status + "): \"" + this.data + "\"", Diagnostics.LogTypes.Success);
                                this.onData.dispatch(this);
                            } else { // (expected 'success' statuses not found, so assume expected data did not load, and skip the data event)
                                Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Success response not received (status: " + this.xmlHttpRequest.status + "). Response data: \"" + this.data + "\"", Diagnostics.LogTypes.Error);
                                this.lastEvent = ev;
                                this.errorMessage = "Success response not received.";
                                this.onError.setTriggerState() && this.onError.dispatch(this, ev);
                            }
                        }
                    };
                    this.xmlHttpRequest.onloadstart = (ev: Event) => {
                        this.lastEvent = ev;
                        this.total = -1;
                        this.loaded = -1;
                        this.onProgress.setTriggerState(this.loaded) && this.onProgress.dispatch(this, this.total, this.loaded);
                    };
                    this.xmlHttpRequest.onloadend = (ev: ProgressEvent) => {
                        /** (called after 'onload' when data download is completed) */
                        this.lastEvent = ev;
                        if (ev && ev.total !== void 0) this.total = ev.total;
                        if (ev && ev.loaded !== void 0) this.loaded = ev.loaded;
                        //??if (ev.lengthComputable) // (note: 'ev.lengthComputable' is true only if the total length was supplied in the header)
                        this.onProgress.setTriggerState(this.loaded) && this.onProgress.dispatch(this, this.total, this.loaded);
                        //??else
                        //??    this.onProgress.setTriggerState(this.loaded) && this.onProgress.dispatch(this, -1, -1);
                    };
                    this.xmlHttpRequest.onabort = (ev: any) => {
                        this.lastEvent = ev;
                        this.errorMessage = "Aborted.";
                        this.onError.setTriggerState() && this.onError.dispatch(this, ev);
                        Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Connection was aborted.", Diagnostics.LogTypes.Warning);
                    };
                    this.xmlHttpRequest.onprogress = (ev: ProgressEvent) => {
                        this.lastEvent = ev;
                        if (ev && ev.total !== void 0) this.total = ev.total;
                        if (ev && ev.loaded !== void 0) this.loaded = ev.loaded;
                        this.onProgress.setTriggerState(this.loaded) && this.onProgress.dispatch(this, this.total, this.loaded);
                    };
                    this.xmlHttpRequest.onreadystatechange = (ev: Event) => { // (note: only this is supported in older browsers; also, doesn't fire in Gecko synch requests)
                        switch (this.xmlHttpRequest.readyState) {
                            case ReadyStates.OPENED:
                                this.onOpen.setTriggerState() && this.onOpen.dispatch(this);
                                break;
                            case ReadyStates.HEADERS_RECEIVED:
                                this.xmlHttpRequest.onloadstart(ev);
                                break;
                            case ReadyStates.LOADING:
                                this.xmlHttpRequest.onprogress(<any>ev);
                                break;
                            case ReadyStates.DONE:
                                this.xmlHttpRequest.onload(<any>ev);
                                break;
                        }
                    };
                    this.xmlHttpRequest.ontimeout = (ev: Event) => {
                        this.errorMessage = "Connection timed out.";
                        this.onError.setTriggerState("timeout") && this.onError.dispatch(this, ev);
                    };

                    this.xmlHttpRequest.open(this.xmlHttpMethod, this.connectionURI, this.asyncRequest, this.userName, this.password);

                    log.write("Opened an AJAX connection; ready for data ...");

                    if (this.asyncRequest)
                        this.xmlHttpRequest.timeout = this.__timeout;

                    log.endCapture();
                }
            }

            // ---------------------------------------------------------------------------------------------------------------

            send(data?: any, dataType: DataTypes = DataTypes.Any) {
                this.total = -1;
                this.loaded = -1;
                this.lastEvent = null;
                Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Sending data: \"" + data + "\"", Diagnostics.LogTypes.Info);
                if (this.xmlHttpRequest) {
                    if (data !== void 0)
                        this.xmlHttpRequest.send(data); // (passes 1 arg)
                    else
                        this.xmlHttpRequest.send(); // (passes 0 args [in case it matters])
                    Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": AJAX data sent.", Diagnostics.LogTypes.Success);
                } else if (this.webSocket) {
                    this.webSocket.send(data);
                    Diagnostics.log(Modules.$_System.Net, this.connectionURI + ": Socket data sent.", Diagnostics.LogTypes.Success);
                }
            }

            // ---------------------------------------------------------------------------------------------------------------
        }

        // ===================================================================================================================

    }
}
